Drop clap in favour of a hand-rolled argument parser#13
Open
Mic92 wants to merge 1 commit intoLordGrimmauld:mainfrom
Open
Drop clap in favour of a hand-rolled argument parser#13Mic92 wants to merge 1 commit intoLordGrimmauld:mainfrom
Mic92 wants to merge 1 commit intoLordGrimmauld:mainfrom
Conversation
This shim is ~90 lines that assemble an argv for run0 and exec() it. Pulling in clap with the derive feature for that brought 21 transitive crates (syn, quote, proc-macro2, anstream, windows-sys, ...) totalling roughly 460k lines of vendored Rust into the build of something that may sit on the system's sudo path. That is a lot of supply-chain surface, lockfile churn and build time for what is, in the end, one getopt loop over a fixed option set defined by sudo(8). This is not a theoretical concern. The xz/liblzma backdoor (CVE-2024-3094) reached sshd not because OpenSSH was careless but because a transitive dependency two hops away was compromised; a program in sudo's seat is exactly the kind of target that makes deep dependency trees worth attacking. The Rust ecosystem has had its own reminders too: the serde_derive precompiled-binary episode showed that even ubiquitous, trusted proc-macro crates can quietly start running opaque code at build time, and typosquats like rustdecimal have shipped malware through crates.io. Every crate in Cargo.lock is a crate someone has to keep watching. Replace it with a small table-driven parser: a single (short, long, takes_value) table feeds one apply() dispatch, so short and long forms share the same code path. It supports exactly what the shim used from clap (clustered shorts, attached and separate values, --opt=val, --, counted -l, repeatable --run0-extra-arg, --preserve-env[=LIST] with require_equals semantics, and unknown options falling through to the command) and is covered by unit tests for each of those edges. Ignored sudo flags are still parsed so they consume their arguments correctly, they just no longer get struct fields. The net result is zero dependencies, the release binary roughly halved (1.2M -> 570K), cold release builds down from ~10s to ~1s, and 21 fewer crates to review on every cargo update, at the cost of about 200 lines of straight-line, unsafe-free code we own and test ourselves.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This shim is ~90 lines that assemble an argv for run0 and exec() it. Pulling in clap with the derive feature for that brought 21 transitive crates (syn, quote, proc-macro2, anstream, windows-sys, ...) totalling roughly 460k lines of vendored Rust into the build of something that may sit on the system's sudo path. That is a lot of supply-chain surface, lockfile churn and build time for what is, in the end, one getopt loop over a fixed option set defined by sudo(8).
This is not a theoretical concern. The xz/liblzma backdoor (CVE-2024-3094) reached sshd not because OpenSSH was careless but because a transitive dependency two hops away was compromised; a program in sudo's seat is exactly the kind of target that makes deep dependency trees worth attacking. The Rust ecosystem has had its own reminders too: the serde_derive precompiled-binary episode showed that even ubiquitous, trusted proc-macro crates can quietly start running opaque code at build time, and typosquats like rustdecimal have shipped malware through crates.io. Every crate in Cargo.lock is a crate someone has to keep watching.
Replace it with a small table-driven parser: a single (short, long, takes_value) table feeds one apply() dispatch, so short and long forms share the same code path. It supports exactly what the shim used from clap (clustered shorts, attached and separate values, --opt=val, --, counted -l, repeatable --run0-extra-arg, --preserve-env[=LIST] with require_equals semantics, and unknown options falling through to the command) and is covered by unit tests for each of those edges. Ignored sudo flags are still parsed so they consume their arguments correctly, they just no longer get struct fields.
The net result is zero dependencies, the release binary roughly halved (1.2M -> 570K), cold release builds down from ~10s to ~1s, and 21 fewer crates to review on every cargo update, at the cost of about 200 lines of straight-line, unsafe-free code we own and test ourselves.